/* Code for finding imports of an exported symbol. Used only by FindAllReferences. */
/* @internal */
namespace ts.FindAllReferences {
    export interface ImportsResult {
        /** For every import of the symbol, the location and local symbol for the import. */
        importSearches: readonly [Identifier, Symbol][];
        /** For rename imports/exports `{ foo as bar }`, `foo` is not a local, so it may be added as a reference immediately without further searching. */
        singleReferences: readonly (Identifier | StringLiteral)[];
        /** List of source files that may (or may not) use the symbol via a namespace. (For UMD modules this is every file.) */
        indirectUsers: readonly SourceFile[];
    }
    export type ImportTracker = (exportSymbol: Symbol, exportInfo: ExportInfo, isForRename: boolean) => ImportsResult;

    /** Creates the imports map and returns an ImportTracker that uses it. Call this lazily to avoid calling `getDirectImportsMap` unnecessarily.  */
    export function createImportTracker(sourceFiles: readonly SourceFile[], sourceFilesSet: ReadonlyMap<true>, checker: TypeChecker, cancellationToken: CancellationToken | undefined): ImportTracker {
        const allDirectImports = getDirectImportsMap(sourceFiles, checker, cancellationToken);
        return (exportSymbol, exportInfo, isForRename) => {
            const { directImports, indirectUsers } = getImportersForExport(sourceFiles, sourceFilesSet, allDirectImports, exportInfo, checker, cancellationToken);
            return { indirectUsers, ...getSearchesFromDirectImports(directImports, exportSymbol, exportInfo.exportKind, checker, isForRename) };
        };
    }

    /** Info about an exported symbol to perform recursive search on. */
    export interface ExportInfo {
        exportingModuleSymbol: Symbol;
        exportKind: ExportKind;
    }

    export const enum ExportKind { Named, Default, ExportEquals }

    export const enum ImportExport { Import, Export }

    type SourceFileLike = SourceFile;
    // Identifier for the case of `const x = require("y")`.
    type Importer = Identifier;
    type ImporterOrCallExpression = Importer | CallExpression;

    /** Returns import statements that directly reference the exporting module, and a list of files that may access the module through a namespace. */
    function getImportersForExport(
        sourceFiles: readonly SourceFile[],
        sourceFilesSet: ReadonlyMap<true>,
        allDirectImports: Map<ImporterOrCallExpression[]>,
        { exportingModuleSymbol, exportKind }: ExportInfo,
        checker: TypeChecker,
        cancellationToken: CancellationToken | undefined,
    ): { directImports: Importer[], indirectUsers: readonly SourceFile[] } {
        sourceFilesSet
        checker
        const markSeenDirectImport = nodeSeenTracker<ImporterOrCallExpression>();
        const markSeenIndirectUser = nodeSeenTracker<SourceFileLike>();
        const directImports: Importer[] = [];
        const isAvailableThroughGlobal = false;
        const indirectUserDeclarations: SourceFileLike[] | undefined = isAvailableThroughGlobal ? undefined : [];

        handleDirectImports(exportingModuleSymbol);

        return { directImports, indirectUsers: getIndirectUsers() };

        function getIndirectUsers(): readonly SourceFile[] {
            if (isAvailableThroughGlobal) {
                // It has `export as namespace`, so anything could potentially use it.
                return sourceFiles;
            }
            // This may return duplicates (if there are multiple module declarations in a single source file, all importing the same thing as a namespace), but `State.markSearchedSymbol` will handle that.
            return indirectUserDeclarations!.map<SourceFile>(getSourceFileOfNode);
        }

        function handleDirectImports(exportingModuleSymbol: Symbol): void {
            const theseDirectImports = getDirectImports(exportingModuleSymbol);
            if (theseDirectImports) {
                for (const direct of theseDirectImports) {
                    if (!markSeenDirectImport(direct)) {
                        continue;
                    }

                    if (cancellationToken) cancellationToken.throwIfCancellationRequested();

                    switch (direct.kind) {
                        case SyntaxKind.CallExpression:
                            if (!isAvailableThroughGlobal) {
                                const parent = direct.parent;
                                if (exportKind === ExportKind.ExportEquals && parent.kind === SyntaxKind.VariableDeclaration) {
                                    const { name } = parent as VariableDeclaration;
                                    if (name.kind === SyntaxKind.Identifier) {
                                        directImports.push(name);
                                        break;
                                    }
                                }

                                // Don't support re-exporting 'require()' calls, so just add a single indirect user.
                                addIndirectUser(direct.getSourceFile());
                            }
                            break;

                        case SyntaxKind.Identifier: // for 'const x = require("y");
                            break; // TODO: GH#23879

                        default:
                            Debug.failBadSyntaxKind(direct, "Unexpected import kind.");
                    }
                }
            }
        }

        function addIndirectUser(sourceFileLike: SourceFileLike): boolean {
            Debug.assert(!isAvailableThroughGlobal);
            const isNew = markSeenIndirectUser(sourceFileLike);
            if (isNew) {
                indirectUserDeclarations!.push(sourceFileLike); // TODO: GH#18217
            }
            return isNew;
        }

        function getDirectImports(moduleSymbol: Symbol): ImporterOrCallExpression[] | undefined {
            return allDirectImports.get(getSymbolId(moduleSymbol).toString());
        }
    }

    /**
     * Given the set of direct imports of a module, we need to find which ones import the particular exported symbol.
     * The returned `importSearches` will result in the entire source file being searched.
     * But re-exports will be placed in 'singleReferences' since they cannot be locally referenced.
     */
    function getSearchesFromDirectImports(directImports: Importer[], exportSymbol: Symbol, exportKind: ExportKind, checker: TypeChecker, isForRename: boolean): Pick<ImportsResult, "importSearches" | "singleReferences"> {
        const importSearches: [Identifier, Symbol][] = [];
        const singleReferences: (Identifier | StringLiteral)[] = [];
        function addSearch(location: Identifier, symbol: Symbol): void {
            importSearches.push([location, symbol]);
        }

        if (directImports) {
            for (const decl of directImports) {
                handleImport(decl);
            }
        }

        return { importSearches, singleReferences };

        function handleImport(decl: Importer): void {
            if (decl.kind === SyntaxKind.Identifier) {
                handleNamespaceImportLike(decl);
                return;
            }
        }

        /**
         * `import x = require("./x")` or `import * as x from "./x"`.
         * An `export =` may be imported by this syntax, so it may be a direct import.
         * If it's not a direct import, it will be in `indirectUsers`, so we don't have to do anything here.
         */
        function handleNamespaceImportLike(importName: Identifier): void {
            // Don't rename an import that already has a different name than the export.
            if (exportKind === ExportKind.ExportEquals && (!isForRename || isNameMatch(importName.escapedText))) {
                addSearch(importName, checker.getSymbolAtLocation(importName)!);
            }
        }

        function isNameMatch(name: string): boolean {
            // Use name of "default" even in `export =` case because we may have allowSyntheticDefaultImports
            return name === exportSymbol.escapedName;
        }
    }

    export type ModuleReference =
        /** "import" also includes require() calls. */
        | { kind: "import", literal: StringLiteralLike }
    export function findModuleReferences(program: Program, sourceFiles: readonly SourceFile[], searchModuleSymbol: Symbol): ModuleReference[] {
        const refs: ModuleReference[] = [];
        const checker = program.getTypeChecker();
        for (const referencingFile of sourceFiles) {
            forEachImport(referencingFile, (_importDecl, moduleSpecifier) => {
                const moduleSymbol = checker.getSymbolAtLocation(moduleSpecifier);
                if (moduleSymbol === searchModuleSymbol) {
                    refs.push({ kind: "import", literal: moduleSpecifier });
                }
            });
        }
        return refs;
    }

    /** Returns a map from a module symbol Id to all import statements that directly reference the module. */
    function getDirectImportsMap(sourceFiles: readonly SourceFile[], checker: TypeChecker, cancellationToken: CancellationToken | undefined): Map<ImporterOrCallExpression[]> {
        const map = createMap<ImporterOrCallExpression[]>();

        for (const sourceFile of sourceFiles) {
            if (cancellationToken) cancellationToken.throwIfCancellationRequested();
            forEachImport(sourceFile, (importDecl, moduleSpecifier) => {
                const moduleSymbol = checker.getSymbolAtLocation(moduleSpecifier);
                if (moduleSymbol) {
                    const id = getSymbolId(moduleSymbol).toString();
                    let imports = map.get(id);
                    if (!imports) {
                        map.set(id, imports = []);
                    }
                    imports.push(importDecl);
                }
            });
        }

        return map;
    }

    /** Iterates over all statements at the top level or in module declarations. Returns the first truthy result. */
    function forEachPossibleImportOrExportStatement<T>(sourceFileLike: SourceFileLike, action: (statement: Statement) => T) {
        return forEach(sourceFileLike.statements, statement => // TODO: GH#18217
            action(statement));
    }

    /** Calls `action` for each import, re-export, or require() in a file. */
    function forEachImport(sourceFile: SourceFile, action: (importStatement: ImporterOrCallExpression, imported: StringLiteralLike) => void): void {
        forEachPossibleImportOrExportStatement(sourceFile, statement => {
            action
            switch (statement.kind) {
                case SyntaxKind.导入声明_: {
                    break;
                }
            }
        });
    }

    export interface ImportedSymbol {
        kind: ImportExport.Import;
        symbol: Symbol;
    }
    export interface ExportedSymbol {
        kind: ImportExport.Export;
        symbol: Symbol;
        exportInfo: ExportInfo;
    }

    /**
     * Given a local reference, we might notice that it's an import/export and recursively search for references of that.
     * If at an import, look locally for the symbol it imports.
     * If at an export, look for all imports of it.
     * This doesn't handle export specifiers; that is done in `getReferencesAtExportSpecifier`.
     * @param comingFromExport If we are doing a search for all exports, don't bother looking backwards for the imported symbol, since that's the reason we're here.
     */
    export function getImportOrExportSymbol(node: Node, symbol: Symbol, checker: TypeChecker, comingFromExport: boolean): ImportedSymbol | ExportedSymbol | undefined {
        return comingFromExport ? getExport() : getExport();
        checker
        function getExport(): ExportedSymbol | ImportedSymbol | undefined {
            const { parent } = node;
            const grandParent = parent.parent;
            if (symbol) {
                if (parent.kind === SyntaxKind.PropertyAccessExpression) {
                    // When accessing an export of a JS module, there's no alias. The symbol will still be flagged as an export even though we're at the use.
                    // So check that we are at the declaration.
                    return symbol.declarations.some(d => d === parent) && isBinaryExpression(grandParent)
                        ? getSpecialPropertyExport(grandParent, /*useLhsSymbol*/ false)
                        : undefined;
                }
            }
            else {
                if (isBinaryExpression(parent)) {
                    return getSpecialPropertyExport(parent, /*useLhsSymbol*/ true);
                }
                else if (isBinaryExpression(grandParent)) {
                    return getSpecialPropertyExport(grandParent, /*useLhsSymbol*/ true);
                }
            }

            function getSpecialPropertyExport(node: BinaryExpression, useLhsSymbol: boolean): ExportedSymbol | undefined {
                useLhsSymbol
                switch (getAssignmentDeclarationKind(node)) {
                    default:
                        return undefined;
                }
            }
        }
    }
}
